Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: Improvements to the experimental full-page navigation #64067

Merged
merged 28 commits into from
Oct 8, 2024

Conversation

michalczaplinski
Copy link
Contributor

@michalczaplinski michalczaplinski commented Jul 29, 2024

This PR brings several improvements to experimental full-page navigation.

Fixes #63880


1. Remove src attributes from prefetched scripts

We have to remove the src attributes of the prefetched scripts because otherwise the contents are ignored. Reported by @luisherranz in #63880 (comment).

2. Use .textContent instead of .innerText to set <script> contents

Prefetched scripts that are inserted in the the <head> have <br> in the DOM:

Screenshot 2024-07-29 at 16 49 54

The article from MDN about innerText explains why we see this problem:

As a setter this will replace the element's children with the given value, converting any line breaks into
elements.

3. populateInitialData() with state from the server

We have to populate the client-side state with the state coming from the server (as mentioned in this comment:

We populate the client-side state with the state from the server on the initial load.
We also re-populate the state when doing region-based navigation. But we don't do the equivalent when doing full page navigations.

4. Wait for load event of the script element

Finally, there is another issue:

Scripts that are added to the DOM with type="module" are deferred by default! This means that if we add a script to the <head> the way we currently do it is not guaranteed to have executed before the HTML of the page is updated.

Notice how the DOM had been updated before the onload handler fires. The error at the end that is caused by it because the JS store that contains the callbacks.initTriggerButton has not yet loaded on the page.

output_87404e.mp4

The solution proposed in this PR follows this comment:

  • For prefetching create <link> elements with modulepreload instead of fetching the modules ourselves.
  • Upon navigation, use import() to evaluate the scripts.

Testing instructions

You can test this PR manually by using blocks that use the Interactivity API and enabling the client-side navigation in GB settings.

Some things to watch out for:

  • Watch out for any client-side console errors.
  • Ensure the JS assets are preloaded correctly (watch the Network tab of your browser's devtools). They should be preloaded on hovering over a link.
  • Ensure that the cache-busting works correctly (as referred to in this comment)
  • Ensure that upon hovering over a link, we only pre-load modules, NOT all scripts.

Example test

  1. Make sure to enable the "full page client-side navigation" experiment in GB settings
  2. Create a post/page with an Image block using the "Lightbox" feature. Let's call it an Image test post.
  3. Navigate to your Blog Archive page.
  4. Hover over the link to the Image test post.
  5. TEST: Ensure that the assets are preloaded correctly.
  6. Click on the link to the Image test post.
  7. TEST: Ensure that the post is navigated to using full-page client-side navigation.
  8. TEST: Click on the image to open the Lightbox. Ensure that it can be opened and closed without issue.
  9. Navigate back to the Blog Archive page.
  10. TEST: Ensure that the navigation was done using full-page client-side navigation.

Several follow-ups should be done:

  1. Figure out the way to only load the scripts for the new & compatible interactive blocks on the page. Previously mentioned in Image block view script causes errors when client-side navigation is enabled #63880 (comment) & Experiment: Add full page client-side navigation experiment setting #59707 (comment)

  2. Refactor the fetchHeadAssets() function so that it's not called inside of regionsToVdom.

  3. Handle CSS asset loading (not done in this PR):

    1. Handle properly the cache busting hash in the URLs of the block styles. See this comment
    2. Moving the styles from a file to <style> tags messes up with how the URLs are interpreted in CSS's url() function: Interactivity API: Improvements to the experimental full-page navigation #64067 (comment)
  4. Occasionally the CSS for the page after a navigation is messed up (see video). I'm not sure if this is due to missing CSS or or file order or something else.

    output_a052aa.mp4

@michalczaplinski michalczaplinski added [Type] Bug An existing feature does not function as intended [Feature] Interactivity API API to add frontend interactivity to blocks. [Packages] Interactivity /packages/interactivity labels Jul 29, 2024
@michalczaplinski michalczaplinski changed the title Fix: Full-page navigation for blocks with server-side defined state Interactivity API: Improvements to the experimental full-page navigation Aug 1, 2024
@michalczaplinski michalczaplinski marked this pull request as ready for review August 1, 2024 18:37
Copy link

github-actions bot commented Aug 1, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: michalczaplinski <[email protected]>
Co-authored-by: DAreRodz <[email protected]>
Co-authored-by: sirreal <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: luisherranz <[email protected]>
Co-authored-by: westonruter <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@luisherranz
Copy link
Member

I believe that this fix should work after #62734 is merged.

What functionality or change from the other pull request does this one need to work?

@michalczaplinski
Copy link
Contributor Author

What functionality or change from the other pull request does this one need to work?

Sorry, this comment is outdated and I've removed it now. I made it before updating this PR . We don't need any specific functionality from that PR.

@luisherranz
Copy link
Member

luisherranz commented Aug 6, 2024

I added a comment here about the general strategy of preloading and waiting for resources to load.

@michalczaplinski
Copy link
Contributor Author

I've just noticed that this:

// wait for the `load` event to fire before appending the element
return new Promise( ( resolve, reject ) => {
  element.onload = resolve;
  element.onerror = reject;
} );

actually breaks the full-page navigation. We can't rely on the onload event I guess. I'll take a look shortly.

@michalczaplinski michalczaplinski marked this pull request as draft August 7, 2024 12:31
@michalczaplinski
Copy link
Contributor Author

I've just noticed that this:

// wait for the `load` event to fire before appending the element
return new Promise( ( resolve, reject ) => {
  element.onload = resolve;
  element.onerror = reject;
} );

actually breaks the full-page navigation. We can't rely on the onload event I guess. I'll take a look shortly.

Just coming back to this. The reason I was seeing breakage is because the load event never fires on inline <script> elements. So something like this does not work:

     const script = document.createElement('script');
     script.textContent = 'console.log("hi");'    
     script.onload = () => { console.log("loaded")}; // This will log
     document.body.append(script);

I'm trying a different approach now:

  1. Instead of loading the content with fetch() and inlining it into <script> and <style> elements, fetch it with <link rel="modulepreload">. For styles, we can use rel="preload" as="style".
  2. In the updateHead() return a Promise waiting for the load of the script.

@luisherranz
Copy link
Member

  • In the updateHead() return a Promise waiting for the load of the script.

As I mentioned here, you don't need to add an actual script and use the load event for the scripts; you can use dynamic imports since all the scripts should be modules.

Comment on lines 57 to 60
export const headElements = new Map<
string,
{ tag: HTMLElement; text?: string }
>();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've exported the headElements so that we don't have to pass it as an argument to fetchHeadAssets().

[ ...headElements.entries() ]
.filter( ( [ , { tag } ] ) => tag.nodeName === 'SCRIPT' )
.map( async ( [ url ] ) => {
await import( /* webpackIgnore: true */ url );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the webpack comment here. Otherwise, webpack will try to code-split here into a new chunk and it does not permit using fully dynamic paths.

@michalczaplinski
Copy link
Contributor Author

michalczaplinski commented Aug 21, 2024

As I mentioned #60951 (comment), you don't need to add an actual script and use the load event for the scripts; you can use dynamic imports since all the scripts should be modules.

Yup, this is what I ended up doing. I think there's nothing wrong with adding the scripts and waiting for the load approach, though.


I think this is ready for review now.

There are 2 issues that I've noticed that should be fixed in follow-ups:

  1. Do not load the scripts that have already been preloaded if they only differ by ?ver=<some-number> query param. I initially thought that this should be fixed when using the production build (npm run build) but it's not the case.

    Fixed in 9aef408

    output_a20672.mp4
  2. Occasionally the CSS for the page after a navigation is messed up (see video). I'm not sure if this is due to missing CSS or or file order or something else.

    output_a052aa.mp4

@michalczaplinski michalczaplinski marked this pull request as ready for review August 21, 2024 15:35
@gziolo
Copy link
Member

gziolo commented Aug 21, 2024

I initially thought that this should be fixed when using the production build (npm run build) but it's not the case.

You should test it with SCRIPT_DEBUG set to false in your wp-config.php file.

@michalczaplinski
Copy link
Contributor Author

I initially thought that this should be fixed when using the production build (npm run build) but it's not the case.

You should test it with SCRIPT_DEBUG set to false in your wp-config.php file.

Nice, I didn't think about it, thanks! 🙂

@michalczaplinski
Copy link
Contributor Author

ok, I've just tried it and it looks like setting SCRIPT_DEBUG to false does not make any difference, unfortunately.

@gziolo
Copy link
Member

gziolo commented Aug 21, 2024

ok, I've just tried it and it looks like setting SCRIPT_DEBUG to false does not make any difference, unfortunately.

This is the reason it uses time() for interactivity router script:

$default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time();

I don't know where the style.css comes from, but there must be something similar present in the code.

@michalczaplinski
Copy link
Contributor Author

michalczaplinski commented Aug 27, 2024

Thanks @gziolo

I figured out that GUTENBERG_VERSION is only available in production. In dev, it's unset.

echo "define( 'GUTENBERG_VERSION', '$plugin_version' );\n";

For production, there is no problem in that case. ver === GUTENBERG_VERSION

In development, we should check if the full-page navigation experiment is enabled. And use the time() only if the experiment is disabled.

@michalczaplinski
Copy link
Contributor Author

In development, we should check if the full-page navigation experiment is enabled. And use the time() only if the full-page navigation experiment is disabled.

I've now added this.

We should look into doing the same for indivudual block's stylesheets:

Screenshot 2024-08-29 at 12 19 32

But this can be done in a follow-up

@luisherranz
Copy link
Member

In development, we should check if the full-page navigation experiment is enabled. And use the time() only if the experiment is disabled.

Won't that cause problems for people developing in Gutenberg with that option enabled?

@michalczaplinski
Copy link
Contributor Author

michalczaplinski commented Aug 29, 2024

Won't that cause problems for people developing in Gutenberg with that option enabled?

I think that if you are developing with the full-page navigation enabled, then you DO want to disable the cache busting. Otherwise, full-page navigation does not work properly. This is how I did it:

$experiments = get_option( 'gutenberg-experiments' );
$full_page_navigation_enabled = isset( $experiments['gutenberg-full-page-client-side-navigation'] );
wp_register_script_module(
'@wordpress/interactivity',
gutenberg_url( '/build/interactivity/' . ( SCRIPT_DEBUG ? 'debug.min.js' : 'index.min.js' ) ),
array(),
$full_page_navigation_enabled ? null : $default_version
);

Am I missing something?

@michalczaplinski michalczaplinski enabled auto-merge (squash) October 8, 2024 14:12
@michalczaplinski michalczaplinski merged commit db22c1b into trunk Oct 8, 2024
62 checks passed
@michalczaplinski michalczaplinski deleted the fix/full-page-navigation-server-state branch October 8, 2024 14:46
@github-actions github-actions bot added this to the Gutenberg 19.5 milestone Oct 8, 2024
@michalczaplinski
Copy link
Contributor Author

Thank you @sirreal ! 🙏

@sirreal
Copy link
Member

sirreal commented Oct 23, 2024

There are several follow-up items from this PR that I don't want to slip through the cracks. Are they tracked anywhere?

@michalczaplinski
Copy link
Contributor Author

Thanks for the vigilance @sirreal :)

Yes, they're tracked in #60951. In particular, see this latest comment.

@luisherranz
Copy link
Member

luisherranz commented Nov 12, 2024

One thing I know is missing and I don't know how to solve is related to the importmap.

Yes, we knew this from the beginning 🙂

My idea is to use something like this for the modules that we load in the new pages.

EDIT: If you're curious about how it works, it's based on the es-module-lexer package.

karthick-murugan pushed a commit to karthick-murugan/gutenberg that referenced this pull request Nov 13, 2024
…ion (WordPress#64067)

* fix: Add initial data population in interactivity router

* chore: Update element.innerText to element.textContent in head.ts file

* feat: Exclude src and href attributes when copying head element attributes

* chore: Populate initial data in interactivity router

* chore: Move Populate initial data in interactivity router

* Wait for `load` event of script element before returning from `fetchHeadAssets()` function

* feat: Update head tags to improve prefetching of scripts and stylesheets

This commit modifies the `updateHead` function in `head.ts` to improve support for lazy loading of scripts and stylesheets. It preloades the script modules using `modulepreload`, imports the necessary scripts using dynamic imports and adds the `preload` link elements for stylesheets.

* Do not load interactivity script modules in development mode when full page navigation is enabled

* Format interactivity-api.php

* Update interactivity script module registration to use version from asset files

- Added logic to retrieve version information from `index.min.asset.php` and `router.min.asset.php` files.
- Updated `wp_register_script_module` calls to use the retrieved version instead of the default version when full-page navigation is not enabled.

* empty commit

* Rename populateInitialData to populateServerData

* remove populateServerData

* try: remove the webpack comment

* try: remove the await import()

* bring back the async import

* Move headElements to head.ts

* Revert "try: remove the webpack comment"

This reverts commit 62e527e.

* default_version => router_version

* Remove the changes to interactivity-api.php

* Make `renderRegions` async

* Update TS type of the stylesheets variable

Co-authored-by: Jon Surrell <[email protected]>

* Replace Array.from() with direct forEach() on NodeList in head.ts:

* Check if href is null

* Clarify why we only prefetch script modules

* Add changelog

---------

Co-authored-by: michalczaplinski <[email protected]>
Co-authored-by: DAreRodz <[email protected]>
Co-authored-by: sirreal <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: luisherranz <[email protected]>
Co-authored-by: westonruter <[email protected]>
@mevanloon
Copy link

mevanloon commented Nov 19, 2024

Regarding the CSS jumbling up, I have a strong suspicion this has to do with the way global styles are generated per block. Those get a suffix generated by wp_unique_prefixed_id(), and you'd get something like wp-container-core-group-is-layout-1. Though, this is not unique to the global site, just the current page. Those suffixes might collide and invalidate the CSS of an earlier page.

I encountered this issue with Swup (a JS framework that is remarkably similar to what the Interactivity API is doing), and 'fixed' it with this incredibly hacky hack:

if(is_wp_version_compatible("6.5")) {
  add_filter('sanitize_title', function($title) {
    if(preg_match("/core-/", $title)) {
      $rand = rand(0, 1000);
      
      for($i=0;$i<$rand;$i++) {
        wp_unique_prefixed_id("wp-container-$title-is-layout-");
      }
    }
    return $title;
  });
}

A more proper fix could involve a filter to be able to change those suffixes—there is not one, at the moment. Such a filter could then be called when the Interactivity API is active.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Interactivity API API to add frontend interactivity to blocks. No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Packages] Interactivity /packages/interactivity [Type] Bug An existing feature does not function as intended
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Image block view script causes errors when client-side navigation is enabled
7 participants